feat(client): local-first cache, virtualized list, optimistic UI#2
Merged
Conversation
Implements the client-side optimization tier from the Email 2.0 proposal (Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over large mailboxes, and instant actions. No backend/protocol changes. - IndexedDB cache (Dexie), scoped per account email, for mailbox-list rows and message bodies. All reads/writes are failure-tolerant so a cache error can never break the app. - Cache-first rendering: inbox/sent/trash paint from IndexedDB before the network responds, then refresh in the background with no blocking spinner. A failed folder fetch no longer clobbers good cached rows. - Cache-first message bodies: reopening a thread paints instantly, then revalidates; a revalidation error won't blank an already-painted body. - Optimistic markAsRead/deleteThread with rollback on failure. - Virtualized ThreadList via @tanstack/react-virtual. Adds deps: dexie, @tanstack/react-virtual.
The inbox previously loaded only the newest 50 messages per folder with no way to see older mail. Adds cursor-based pagination on top of the virtualized list: - DataContext tracks a per-folder `before` UID cursor, `hasMore`, and `loadingMore`, and exposes loadMore(role). loadAll records the first page's cursor; loadMore fetches the next page (de-duped) and appends to state + IndexedDB cache. - ThreadList auto-loads the next page when the last row scrolls into view (react-virtual), with a footer spinner while fetching. - Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed while a search query is active (client-side search only filters loaded rows). The backend already supported the `before` cursor + next_before.
…scroll Replaces the append-on-scroll pager with classic page controls: a "start–end of total" label and prev/next arrows above the list. Backend: - imap.ListMessages also returns the mailbox total (mbox.Messages); the messages endpoint includes it as `total` for the "of N" count. Frontend: - DataContext tracks page/total/pageLoading per folder plus a per-page `before` cursor history (ref), and exposes nextPage/prevPage that REPLACE the visible page (forward cursor discovered from next_before, back reuses the stored cursor). Only page 0 is mirrored to the cache. - ThreadList renders the pager bar above the virtualized scroll area and resets scroll to top on page change. - Inbox/Sent/Trash wire prev/next for their role; the pager is hidden while a search query is active (search filters the loaded page only).
maneeshaxyz
pushed a commit
to maneeshaxyz/quicksilver
that referenced
this pull request
Jun 15, 2026
…LK#2) * feat(client): local-first cache, virtualized list, optimistic UI Implements the client-side optimization tier from the Email 2.0 proposal (Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over large mailboxes, and instant actions. No backend/protocol changes. - IndexedDB cache (Dexie), scoped per account email, for mailbox-list rows and message bodies. All reads/writes are failure-tolerant so a cache error can never break the app. - Cache-first rendering: inbox/sent/trash paint from IndexedDB before the network responds, then refresh in the background with no blocking spinner. A failed folder fetch no longer clobbers good cached rows. - Cache-first message bodies: reopening a thread paints instantly, then revalidates; a revalidation error won't blank an already-painted body. - Optimistic markAsRead/deleteThread with rollback on failure. - Virtualized ThreadList via @tanstack/react-virtual. Adds deps: dexie, @tanstack/react-virtual. * feat(client): infinite-scroll pagination beyond the first 50 messages The inbox previously loaded only the newest 50 messages per folder with no way to see older mail. Adds cursor-based pagination on top of the virtualized list: - DataContext tracks a per-folder `before` UID cursor, `hasMore`, and `loadingMore`, and exposes loadMore(role). loadAll records the first page's cursor; loadMore fetches the next page (de-duped) and appends to state + IndexedDB cache. - ThreadList auto-loads the next page when the last row scrolls into view (react-virtual), with a footer spinner while fetching. - Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed while a search query is active (client-side search only filters loaded rows). The backend already supported the `before` cursor + next_before. * feat: Gmail-style paged navigation ("1–50 of N") instead of infinite scroll Replaces the append-on-scroll pager with classic page controls: a "start–end of total" label and prev/next arrows above the list. Backend: - imap.ListMessages also returns the mailbox total (mbox.Messages); the messages endpoint includes it as `total` for the "of N" count. Frontend: - DataContext tracks page/total/pageLoading per folder plus a per-page `before` cursor history (ref), and exposes nextPage/prevPage that REPLACE the visible page (forward cursor discovered from next_before, back reuses the stored cursor). Only page 0 is mirrored to the cache. - ThreadList renders the pager bar above the virtualized scroll area and resets scroll to top on page change. - Inbox/Sent/Trash wire prev/next for their role; the pager is hidden while a search query is active (search filters the loaded page only).
maneeshaxyz
pushed a commit
to maneeshaxyz/quicksilver
that referenced
this pull request
Jun 15, 2026
…LK#2) * feat(client): local-first cache, virtualized list, optimistic UI Implements the client-side optimization tier from the Email 2.0 proposal (Phase 2 + parts of Phase 5): instant warm loads, smooth scrolling over large mailboxes, and instant actions. No backend/protocol changes. - IndexedDB cache (Dexie), scoped per account email, for mailbox-list rows and message bodies. All reads/writes are failure-tolerant so a cache error can never break the app. - Cache-first rendering: inbox/sent/trash paint from IndexedDB before the network responds, then refresh in the background with no blocking spinner. A failed folder fetch no longer clobbers good cached rows. - Cache-first message bodies: reopening a thread paints instantly, then revalidates; a revalidation error won't blank an already-painted body. - Optimistic markAsRead/deleteThread with rollback on failure. - Virtualized ThreadList via @tanstack/react-virtual. Adds deps: dexie, @tanstack/react-virtual. * feat(client): infinite-scroll pagination beyond the first 50 messages The inbox previously loaded only the newest 50 messages per folder with no way to see older mail. Adds cursor-based pagination on top of the virtualized list: - DataContext tracks a per-folder `before` UID cursor, `hasMore`, and `loadingMore`, and exposes loadMore(role). loadAll records the first page's cursor; loadMore fetches the next page (de-duped) and appends to state + IndexedDB cache. - ThreadList auto-loads the next page when the last row scrolls into view (react-virtual), with a footer spinner while fetching. - Inbox/Sent/Trash wire loadMore for their role. Pagination is suppressed while a search query is active (client-side search only filters loaded rows). The backend already supported the `before` cursor + next_before. * feat: Gmail-style paged navigation ("1–50 of N") instead of infinite scroll Replaces the append-on-scroll pager with classic page controls: a "start–end of total" label and prev/next arrows above the list. Backend: - imap.ListMessages also returns the mailbox total (mbox.Messages); the messages endpoint includes it as `total` for the "of N" count. Frontend: - DataContext tracks page/total/pageLoading per folder plus a per-page `before` cursor history (ref), and exposes nextPage/prevPage that REPLACE the visible page (forward cursor discovered from next_before, back reuses the stored cursor). Only page 0 is mirrored to the cache. - ThreadList renders the pager bar above the virtualized scroll area and resets scroll to top on page change. - Inbox/Sent/Trash wire prev/next for their role; the pager is hidden while a search query is active (search filters the loaded page only).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the client-side optimization tier from the Email 2.0 proposal — Phase 2 (client cache + cache-first render) plus parts of Phase 5 (virtualization + optimistic UI). Pure frontend; no backend or IMAP protocol changes. Gives the most immediately visible wins: instant warm loads, smooth scrolling over large mailboxes, and instant actions.
What's included
src/nonview/cache/db.ts, Dexie) —threadsand message-bodiestables, scoped per account email so a shared browser never mixes mailboxes. Every read/write is failure-tolerant: a cache error can never break the app (IMAP remains the source of truth).DataContext) — inbox/sent/trash paint from IndexedDB before the network responds, then refresh in the background with no blocking spinner on a warm cache. A failed folder fetch no longer clobbers good cached rows with an empty list.ThreadPage+getCachedMessages) — reopening a thread paints instantly, then revalidates; a revalidation error won't blank an already-painted body.markAsRead/deleteThreadupdate screen + cache instantly and reconcile with the server, rolling back on failure.ThreadList,@tanstack/react-virtual) — only on-screen rows mount; smooth scroll over large mailboxes.New deps:
dexie,@tanstack/react-virtual.Targets addressed (proposal §3)
Verification
npm run buildpassesnpm run typecheckclean for the new/changed TS filesNot yet covered: full end-to-end login against a live mailbox (requires the backend running).
Out of scope (follow-up increments)